Lecture 8¶
We'll do some more with classes.
References:
- The Sage Tutorial on objects and classes
- Chapter 11 of Mohit, and Bhaskar N. Das. Learn Python in 7 Days : Learn Efficient Python Coding Within 7 Days, Packt Publishing, Limited, 2017.
- The Python Tutorial: Chapter 9: Classes
An empty class¶
To think a bit more about classes, maybe it is useful to play around with the simplest class:
class EmptyClass:
pass
The pass
statement is used to express an empty code block. We've declared an EmptyClass
, but there is nothing in it.
We can construct an instance of this class:
e = EmptyClass()
e
<__main__.EmptyClass object at 0x7f8fb3e64210>
When e
is printed, it indicates it is an EmptyClass
object and has a memory location assigned to it. We can use this memory to store values, similar to a dictionary. For example:
e.a = sqrt(2)
e.a
sqrt(2)
We could even store a function in it. Such as:
def double(x):
return 2*x
e.d = double
e.d
<function double at 0x7f8f387ec860>
e.d(3)
6
Instances of the class are individual objects. Typically you'd want them to have common features. The way I've been doing things above does not ensure this however. For example, if we construct another instance, it has no access to e.a
or e.d
:
e2 = EmptyClass()
e2
<__main__.EmptyClass object at 0x7f8f387fcb50>
e2.a
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[7], line 1 ----> 1 e2.a AttributeError: 'EmptyClass' object has no attribute 'a'
e2.d
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[8], line 1 ----> 1 e2.d AttributeError: 'EmptyClass' object has no attribute 'd'
Despite that an empty class is empty, there are a lot of items in it. The dir
function lists the attributes of an object. (The list dir(obj)
things that can be obtained/done with the object e.g., obj.add(obj2)
.) Here's what we get when we do dir(EmptyClass)
:
dir(EmptyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
There are a lot of commands above, all surrounded by two underscores. Our class is not really empty, these functions have been provided with default values. Here is some of their meanings:
__init__
: For setting variable values in new instances. This process is called initialization.__repr__
: Returns a string representation of the object intended for the programmer (e.g, for debugging).__str__
: Returns a string representation of the object intended to be easier to read than__repr__
.__eq__
: For checking for equality of objects.__hash__
: Allows the object to define a hash (for use in dictionaries and other data structures).
This seems good for now. These are probably the most common functions you would want to override. Overriding means we'll replace their default values with something more useful to us. The first function you'd want to override is __init__
, so you can construct objects in a class in a more uniform way than the above.
The projective plane¶
We can define the projective plane over any field, $F$. (More generally, you can define the projective plane over any division ring, but we'll focus on Fields.) The projective plane consists of equivalence classes of non-zero vectors in $F^3$. Two non-zero elements $x=(x_1, x_2, x_3)$ and $y=(y_1, y_2, y_3)$ are equivalent if there is a $k \in F$ such that $kx=y$, where $k$ is acting by scalar multiplication.
These equivalence classes of vectors are known as points. The equivalence class makes up a line through the origin $(0,0,0)$ with the origin removed. Points are organized in lines. Our goal is to write three classes:
ProjectivePlane
: This object represents the full plane. It is important that it will keep track of the field. (There are multiple projective planes, one for each field.)ProjectivePoint
: Which will represent a point in a projective plane. We started work on this last week.ProjectiveLine
: Which will represent a line in a projective plane.
We want to be able to do things like this:
- Given two points, use
point1.join(point2)
to construct the line through the two points. - Given two lines, use
line1.intersect(line2)
to construct the intersection. - Print points and lines in a meaningful way.
- Test these objects for equality.
- Plot points and lines.
- Use them as keys in a dictionary (so for example we could store what color we'd like a line to be).
Overriding the __init__
method¶
The __init__
method has the following form:
def __init__(self, parameters...):
# Commands to initialize self
The self object is provided by Python: It is an empty object. The programmers job is to add attributes to this object.
Our ProjectivePlanes
are determined by a field. Here we write an __init__
method that has one parameter. We store this parameter in the self
object using the name _field
. The underscore in the variable name indicates (by convention) that the value should not typically be directly accessed by the user, and instead is for internal use of the class/object.
class ProjectivePlane:
def __init__(self, field):
self._field = field
Now to construct a ProjectivePlane
we must pass a field. Again, Python provides self
to the __init__
method. We don't call __init__
directly here. Instead we use ProjectivePlane(field)
. For example:
rational_plane = ProjectivePlane(QQ)
rational_plane
<__main__.ProjectivePlane object at 0x7f8f37981410>
Above we can see that RP2
is a ProjectivePlane
and its memory location. We can access the “internal” variable _field
if we wish:
rational_plane._field
Rational Field
Another example:
plane2 = ProjectivePlane(RR)
plane2
<__main__.ProjectivePlane object at 0x7f8f387cf010>
plane2._field
Real Field with 53 bits of precision
Defining methods¶
A method is a function we associate to an object in a class. The first parameter should always be an object in the class, and by convention is called self
.
We said above that the _field
variable is for internal use, but the user might want to know what it is. So, we'll define a field
method that returns it. Here is the updated class:
class ProjectivePlane:
def __init__(self, field):
self._field = field
def field(self):
return self._field
Since we changed the class we should construct a new member:
rational_plane = ProjectivePlane(QQ)
rational_plane
<__main__.ProjectivePlane object at 0x7f8f37933e10>
There are two ways to call the method field
. First we can use the class:
ProjectivePlane.field(rational_plane)
Rational Field
The more natural way to do this is to call rational_plane.field()
.
rational_plane.field()
Rational Field
These two ways of calling the function do the same thing. Essentially the call rational_plane.field()
is converted into ProjectivePlane.field(rational_plane)
by taking the object before the period and adding it as the first argument to the field
function from the class of rational_plane
.
Adding a second method
Note that the projective plane with field $F$ is closely related to the vector space $F^3$. It is reasonable for the class to make available this vector space. We add a _vector_space
variable to our projective planes, and add a method to access it below:
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
Let's test it.
rational_plane = ProjectivePlane(QQ)
rational_plane.vector_space()
Vector space of dimension 3 over Rational Field
The more verbose way to call it:
ProjectivePlane.vector_space(rational_plane)
Vector space of dimension 3 over Rational Field
Note that above, I included a string in the first line of the definition of vector_space
. This provides documentation for the method. For example:
rational_plane.vector_space?
Signature: rational_plane.vector_space() Docstring: Return the vector space F^3 where F is the base field. Init docstring: Initialize self. See help(type(self)) for accurate signature. File: /tmp/ipykernel_986413/1112186716.py Type: method
The string methods¶
Here we add the methods that display an object as a string. There are two of them, repr()
and str()
.
Before we improve our class, why don't we take a look at these functions. For example, we'll import the datetime
module from the Python libary and get the current time.
import datetime
now = datetime.datetime.now()
The str
representation of now is supposed to be human readable:
str(now)
'2024-09-25 10:49:38.305052'
The repr
representation of now gives a way of reconstructing it:
repr(now)
'datetime.datetime(2024, 9, 25, 10, 49, 38, 305052)'
If we run datetime.datetime(2024, 9, 24, 19, 42, 0, 582632)
we get something equal to now:
other_time = datetime.datetime(2024, 9, 24, 19, 42, 0, 582632)
other_time == now
False
Note that if an object is left at the end of a code block in Jupyter, the string printed is obtained from repr()
:
now
datetime.datetime(2024, 9, 25, 10, 49, 38, 305052)
On the other hand if you print an object, you get the string from str()
:
print(now)
2024-09-25 10:49:38.305052
Remark: In Python, the convention seems to be that repr()
should produce a string allowing the programmer to reproduce the object. In SageMath, the convention seems to be different, and most objects seem to produce the same thing when you apply str()
and when you apply repr()
. We'll try to follow Python's convention with repr()
.
We'll now add __repr__
and __str__
methods to our class. They just need to return string representations.
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
rational_plane = ProjectivePlane(QQ)
repr(rational_plane)
'ProjectivePlane(Rational Field)'
str(rational_plane)
'Projective plane over Rational Field'
rational_plane
ProjectivePlane(Rational Field)
print(rational_plane)
Projective plane over Rational Field
Checking objects for equality¶
The __eq__
method tests two objects for equality. Given the statement a == b
, assuming a
is an object, it calls
a.__eq__(b)
An important issue is that the object b
might not have the same type as a
(i.e., they might be defined by different classes). In this case we would want to return False
. Our implementation of equals for ProjectivePlane
uses type
which returns the class of an object.
The equals method is described in greater detail in the Python reference.
type(False)
<class 'bool'>
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
def __eq__(self, other):
if type(self) != type(other):
return False
# Now we know the objects are both ProjectivePlanes
return self.field() == other.field()
rational_plane1 = ProjectivePlane(QQ)
rational_plane2 = ProjectivePlane(QQ)
rational_plane1 == rational_plane2
True
The above is the same as calling either of the following two commands:
rational_plane1.__eq__(rational_plane2)
True
ProjectivePlane.__eq__(rational_plane1, rational_plane2)
True
Some other cases:
rational_plane3 = ProjectivePlane(RR)
rational_plane1 == rational_plane3
False
rational_plane1 == QQ
False
rational_plane1 == pi
False
Note that the not equal operator !=
is the logical negation of the equals operator (by default):
rational_plane1 != rational_plane2
False
rational_plane3 != 2
True
Implementing a hash¶
We might like to implement a hash so that objects can be used as keys in a dictionary. Currently, we can not do this:
d = {1: 17}
d[rational_plane1] = 5
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[45], line 2 1 d = {Integer(1): Integer(17)} ----> 2 d[rational_plane1] = Integer(5) TypeError: unhashable type: 'ProjectivePlane'
The reason is that hash(rational_plane1)
is not defined:
hash(rational_plane1)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[46], line 1 ----> 1 hash(rational_plane1) TypeError: unhashable type: 'ProjectivePlane'
A hash needs to return an int
. It needs to satisfy the rule that obj1 == obj2
implies hash(obj1) == hash(obj1)
. Ideally you want also that obj1 != obj2
implies hash(obj1) != hash(obj1)
, but this is difficult to guarantee. (The hash outputs a Python int so there are only finitely many choices.)
A hash collision is when you have obj1 != obj2
implies hash(obj1) == hash(obj1)
. Hash colisions will make data structures like dictionaries run slower, so they are best avoided. You can make hash collisions rare if you define your hash somewhat randomly.
Fortunately, immutable Sage objects typically already have a hash. So do tuples and immutable objects built into python. I like to represent the data of my object in a tuple and then take the hash of the tuple.
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
def __eq__(self, other):
if type(self) != type(other):
return False
# Now we know the objects are both ProjectivePlanes
return self.field() == other.field()
def __hash__(self):
# Return the hash of the pair consisting of 'ProjectivePlane' and the field:
return hash( (ProjectivePlane, self._field()) )
Testing the hash properties:
rational_plane1 = ProjectivePlane(QQ)
hash(rational_plane1)
1534545959007253516
rational_plane2 = ProjectivePlane(QQ)
hash(rational_plane2) == hash(rational_plane1)
True
algebraic_plane = ProjectivePlane(AA)
hash(algebraic_plane) == hash(rational_plane1)
False
Testing their use in dictionaries:
d = {}
d[rational_plane1] = 3
d[rational_plane2] = 4
d
{ProjectivePlane(Rational Field): 4}
d[algebraic_plane] = 5
d
{ProjectivePlane(Rational Field): 4, ProjectivePlane(Algebraic Real Field): 5}
ProjectivePoint¶
Here is the ProjectivePoint
class from class last time:
class ProjectivePoint:
V = VectorSpace(QQ, 3)
def __init__(self, v):
v = ProjectivePoint.V(v)
assert v != ProjectivePoint.V.zero()
self.v = v
def x(self):
if self.v[2]!=0:
return self.v[0] / self.v[2]
return Infinity
def y(self):
if self.v[2]!=0:
return self.v[1] / self.v[2]
return Infinity
Let's change it to store a copy of a ProjectivePlane
. The ProjectivePlane
will provide our vector space. The following additional changes were made:
- Changed the name of the stored vector to
self._v
(to indicate thatv
is private). - Added some docmentation for methods.
- We now raise errors when bad parameters are provided. The new things here are:
- We use isinstance to check if the provided
plane
is aProjectivePlane
. - We raise a TypeError if the type of
plane
is incorrect. - We raise a ValueError if the vector passed is the zero vector.
- For more on the error types, see the Python documentation.
- We use isinstance to check if the provided
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self._v[2]!=0:
return self._v[0] / self._v[2]
return Infinity
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self._v[2]!=0:
return self._v[1] / self._v[2]
return Infinity
Testing:
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point.x(), point.y()
(1/3, 2/3)
Adding is_infinite()
, vector()
, and plane()
.¶
The projective plane is an extension of the usual plane. We can think of the usual plane ${\mathbb R}^2$ as consisting of equivalence classes of points of the form $(x, y, 1)$. Every equivalence class is represented except those where the last coordinate of the vector is zero. Points where vectors in the equivalence class have the last coordinate zero are considered to be at infinity in the plane. This is useful as we can already see from the x()
and y()
methods. We add it below.
We also added a vector
method which returns the underlying vector. Since we don't want the user to be able to change it, we set the vector to immutable in the constructor.
Finally we will give a way for the class to return the projective plane that contains it.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
Testing:
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 0])
point.is_infinite()
True
point.vector()
(1, 2, 0)
point.plane()
ProjectivePlane(Rational Field)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point.is_infinite()
False
point.vector()
(1, 2, 3)
point.plane()
ProjectivePlane(Rational Field)
Adding string methods¶
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 0])
point
ProjectivePoint(ProjectivePlane(Rational Field), (1, 2, 0))
print(point)
common point at infinity of lines of slope 2
rational_plane = ProjectivePlane(QQ)
point = ProjectivePoint(rational_plane, [1, 2, 3])
point
ProjectivePoint(ProjectivePlane(Rational Field), (1, 2, 3))
print(point)
(1/3, 2/3)
Implementing equals¶
Recall the equivalence relation on $F^3 \setminus \{{\mathbf 0}\}$: We say $\mathbf x$ and $\mathbf y$ are equivalent if there is a non-zero $c \in F$ such that $c \mathbf x = \mathbf y$. So we need two ProjectivePoints to be equal if they satisfy this for their vectors. If $i$ is an index such that $x_i \neq 0$, then the ratio $c$ must have the form $\frac{y_i}{x_i}$ if they are to be equal.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
def __eq__(self, other):
if not isinstance(other, ProjectivePoint):
return False
if self._v[0] != 0:
ratio = other._v[0] / self._v[0]
elif self._v[1] != 0:
ratio = other._v[1] / self._v[1]
else:
# It is not allowed for all 3 entries to be zero
ratio = other._v[2] / self._v[2]
return ratio*self._v == other._v
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point2 = ProjectivePoint(rational_plane, [2, 4, 6])
point1 == point2
True
point1 = ProjectivePoint(rational_plane, [0, 2, 3])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
point1 == point2
True
point1 = ProjectivePoint(rational_plane, [0, 2, 5])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
point1 == point2
False
point1 = ProjectivePoint(rational_plane, [0, 0, 5])
point2 = ProjectivePoint(rational_plane, [0, 0, 3/2])
point1 == point2
True
Implementing the hash¶
Recall the hash needs to satisfy point1 == point2
implies hash(point1) == hash(point2)
. To arrange this, we will product a “canonical representative” of the equivalence class represented by a point. Then we will hash this canonical representative.
We'll say a vector $v=(v_0, v_1, v_2)$ in $F^3$ is a canonical representative of its equivalence class if its first non-zero entry is one. Every equivalence class has exactly one canonical representative. For example, the canonical representative of the equivalence class of $(0, 7, -3)$ is $(0, 1, \frac{-3}{7})$.
class ProjectivePoint:
def __init__(self, plane, v):
'Construct a point from a ProjectivePlane and a 3-dimensional vector.'
# Raise an error if ProjectivePlane is not a ProjectivePlane
if not isinstance(plane, ProjectivePlane):
# Raise a TypeError which indicates that the parameter was the wrong type
raise TypeError('The parameter plane must be a ProjectivePlane')
self._plane = plane # Store the projective plane
v = self._plane.vector_space()(v) # Convert v into the vector space.
if v == self._plane.vector_space().zero():
# Raise a ValueError indicating the vecotr is unacceptable
raise ValueError('The vector can not be zero')
v.set_immutable() # Make it so the vector cannot be changed.
self._v = v # Store the vector.
def vector(self):
r'Return a vector representing this point.'
return self._v
def is_infinite(self):
'Return True if this point is on the line at infinity in the projective plane.'
return self._v[2] == 0
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def x(self):
'Return the x-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[0] / self._v[2]
def y(self):
'Return the y-coordinate or infinity if the point lies at infinity.'
if self.is_infinite():
return Infinity
return self._v[1] / self._v[2]
def __repr__(self):
return f'ProjectivePoint({repr(self._plane)}, {repr(self._v)})'
def __str__(self):
if self.is_infinite():
if self._v[0] != 0:
m = self._v[1] / self._v[0]
return f'common point at infinity of lines of slope {m}'
else:
return 'common point at infinity of vertical lines'
return f'({self.x()}, {self.y()})'
def __eq__(self, other):
if not isinstance(other, ProjectivePoint):
return False
if self._v[0] != 0:
ratio = other._v[0] / self._v[0]
elif self._v[1] != 0:
ratio = other._v[1] / self._v[1]
else:
# It is not allowed for all 3 entries to be zero
ratio = other._v[2] / self._v[2]
return ratio*self._v == other._v
def __hash__(self):
# cv will be the canonical representative of the equivalence class of self._v
if self._v[0] != 0:
cv = (1/self._v[0]) * self._v
elif self._v[1] != 0:
cv = (1/self._v[1]) * self._v
else:
cv = (1/self._v[2]) * self._v
# So we can hash cv, we set it to be immutable:
cv.set_immutable()
return hash((ProjectivePoint, cv))
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point2 = ProjectivePoint(rational_plane, [2, 4, 6])
hash(point1) == hash(point2)
True
point1 = ProjectivePoint(rational_plane, [0, 2, 3])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
hash(point1) == hash(point2)
True
point1 = ProjectivePoint(rational_plane, [0, 2, 5])
point2 = ProjectivePoint(rational_plane, [0, 1, 3/2])
hash(point1) == hash(point2)
False
point1 = ProjectivePoint(rational_plane, [0, 0, 5])
point2 = ProjectivePoint(rational_plane, [0, 0, 3/2])
hash(point1) == hash(point2)
True
Testing membership¶
Sage parents are often sets. Many Python objects are also containers (sets, tuples, dictionaries, lists, ...). Python has an in
keyword for testing membership. Examples below:
5 in [1, 2, 5]
True
3/2 in QQ
True
In our settings, we can consider a ProjectivePoint
to be in a ProjectivePlane
if the plane
passed to the point construtor equals the projective plane in question.
This particular case is not so interesting. But we plane to implement lines as well, and it is reasonable to ask if a point belongs to a line.
You can implement membership tests with the special __contains__
method. This would be defined in the container had have the form:
def __contains__(self, item):
# Return True if item is in self, False otherwise
So, a command such as x in S
gets converted to the method call S.__contains__(x)
. Let's update our ProjectivePlane
class:
class ProjectivePlane:
def __init__(self, field):
self._field = field
self._vector_space = VectorSpace(field, 3)
def field(self):
'Return the field over which this projective plane is defined.'
return self._field
def vector_space(self):
'Return the vector space F^3 where F is the base field.'
return self._vector_space
def plane(self):
'Return the ProjectivePlane containing this point'
return self._plane
def __repr__(self):
return f'ProjectivePlane({repr(self._field)})'
def __str__(self):
return f'Projective plane over {str(self._field)}'
def __eq__(self, other):
if type(self) != type(other):
return False
# Now we know the objects are both ProjectivePlanes
return self.field() == other.field()
def __hash__(self):
# Return the hash of the pair consisting of 'ProjectivePlane' and the field:
return hash( (ProjectivePlane, self._field()) )
def __contains__(self, item):
# Currently the only thing in a ProjectivePlane is a ProjectivePoint
# Later we will check other types (like Lines).
if isinstance(item, ProjectivePoint):
return self == item.plane()
return False
We test it:
rational_plane = ProjectivePlane(QQ)
point1 = ProjectivePoint(rational_plane, [1, 2, 3])
point1 in rational_plane
True
Remark. Sage gives the ability to do more general membership testing. For example points in ProjectivePlane(QQ)
would be contained in ProjectivePlane(AA)
since the rationals is a subfield of the Field of Algebraic real numbers, so a point in ProjectivePlane(QQ)
could be considered to be in
ProjectivePlane(AA)
. Sage handles these tests and conversions like this (from ProjectivePlane(QQ)
to ProjectivePlane(AA)
) via coercion. This is somewhat technical, but you can read about in the SageMath reference on Coercion. We might get to this later in the course, but for now we will content ourselves with classes defined at the Python level.
Anyway, currently the following returns False:
algebraic_plane = ProjectivePlane(QQ)
point1 in algebraic_plane
False